package com.hulk.rpc.remote;

import com.hulk.rpc.annotation.HulkService;
import com.hulk.rpc.config.server.MethodConfig;
import com.hulk.rpc.invoke.FailoverInvokerFactory;
import com.hulk.rpc.invoke.Invoker;
import com.hulk.rpc.invoke.InvokerUtils;
import com.hulk.rpc.param.MethodParam;
import com.hulk.rpc.param.MethodParamClassFactory;
import com.hulk.rpc.transport.client.App;
import javassist.*;
import lombok.extern.slf4j.Slf4j;

import java.io.Closeable;
import java.io.IOException;
import java.lang.reflect.Field;
import java.lang.reflect.Method;
import java.lang.reflect.Modifier;
import java.util.Collection;
import java.util.List;
import java.util.UUID;
import java.util.concurrent.CompletableFuture;
import java.util.concurrent.ConcurrentHashMap;
import java.util.stream.Collectors;
import java.util.stream.Stream;

/**
 * 注册远程服务（生成对应的class，继承）
 *
 * @author xuxukang
 * @date 2019-04-27 14:47
 */
@Slf4j
public class RemoteServiceFactory implements Closeable {

  private final FailoverInvokerFactory failoverInvokerFactory;
  // 低频使用
  private final ConcurrentHashMap<Class<?>, Object> remoteServiceMap = new ConcurrentHashMap<>();

  public RemoteServiceFactory() {
    this(null);
  }

  public RemoteServiceFactory(FailoverInvokerFactory failoverInvokerFactory) {
    this.failoverInvokerFactory = failoverInvokerFactory;
  }

  @SuppressWarnings("unchecked")
  public <T> T getService(Class<T> clazz) {
    return (T) remoteServiceMap.get(clazz);
  }

  @Override
  public void close() throws IOException {
    // ignore
  }

  public synchronized void register(App app, Class<?> clazz) throws Exception {
    if (app == null) {
      throw new RuntimeException("app must not be null");
    }

    if (clazz == null) {
      throw new RuntimeException("clazz must not be null");
    }

    if (getService(clazz) != null) {
      return;
    }

    Method[] allMethods = clazz.getMethods();
    Collection<MethodConfig> configs = Stream
            .of(allMethods)
            .filter(m -> Modifier.isPublic(m.getModifiers()))
            .filter(m -> !Modifier.isStatic(m.getModifiers()))
            .peek(m -> {
              if (!CompletableFuture.class.equals(m.getReturnType())) {
                throw new RuntimeException("method return-type must be CompletableFuture, "
                        + InvokerUtils.getServiceMethodName("", "", m));
              }
            })//
            .map(MethodConfig::new)
            .collect(Collectors.toList());

    register(app, clazz, configs);
  }

  public synchronized void register(App app, Class<?> clazz, Collection<MethodConfig> configs) throws Exception {

    if (app == null) {
      throw new RuntimeException("app must not be null");
    }

    if (clazz == null) {
      throw new RuntimeException("clazz must not be null");
    }

    if (!app.isSupport(clazz)) {
      throw new RuntimeException("the remote service not support the service " + clazz.getName());
    }

    if (configs == null || configs.isEmpty()) {
      throw new RuntimeException("configs must not be empty");
    }

    if (getService(clazz) != null) {
      return;
    }

    HulkService classConfig = clazz.getAnnotation(HulkService.class);
    if (classConfig != null && classConfig.ignore()) {
      if (log.isInfoEnabled()) {
        log.info(clazz + " service ignore");
      }

      return;
    }

    if (log.isInfoEnabled()) {
      log.info("service " + clazz.getName() + " register");
    }

    Object service = generateRemoteObject(app, clazz, configs);
    remoteServiceMap.put(clazz, service);

    if (failoverInvokerFactory == null) {
      return;
    }
  }

  private Object generateRemoteObject(App app, Class<?> clazz, Collection<MethodConfig> configs) throws Exception {

    if (app == null) {
      throw new RuntimeException("app must not be null");
    }

    if (clazz == null) {
      throw new RuntimeException("clazz must not be null");
    }

    if (configs == null || configs.isEmpty()) {
      throw new RuntimeException("configs must not be empty");
    }

    for (MethodConfig config : configs) {
      if (config == null) {
        throw new RuntimeException("config must not be null");
      }

      if (config.method == null) {
        throw new RuntimeException("config.method must not be null");
      }

      if (config.timeout < 1) {
        throw new RuntimeException("config.timeout must > 0");
      }

      if (config.timeout > 5 * 60 * 1000) {
        throw new RuntimeException("config.timeout must < 5 * 60 * 1000 (5 mintues)");
      }
    }

    Method[] allMethods = clazz.getMethods();
    List<Method> allPublicMethods = Stream//
            .of(allMethods)//
            .filter(m -> Modifier.isPublic(m.getModifiers()))//
            .filter(m -> !Modifier.isStatic(m.getModifiers()))//
            .peek(m -> {
              if (!CompletableFuture.class.equals(m.getReturnType())) {
                throw new RuntimeException("method return-type must be CompletableFuture, "
                        + InvokerUtils.getServiceMethodName("", "", m));
              }
            })//
            .collect(Collectors.toList());

    if (configs.size() != allPublicMethods.size()) {
      throw new RuntimeException("configs must contains all the interface's methods");
    }

    for (Method method : allMethods) {
      if (!CompletableFuture.class.equals(method.getReturnType())) {
        throw new RuntimeException("method return-type must be CompletableFuture, " + method);
      }

      if (!configs.stream().anyMatch(config -> config.method.equals(method))) {
        throw new RuntimeException("configs must contains the method: " + method);
      }
    }

    final String remoteClassName = clazz.getName() + "_RemoteService_"//
            + UUID.randomUUID().toString().replace("-", "");

    // 创建类
    ClassPool pool = ClassPool.getDefault();
    CtClass remoteCtClass = pool.makeClass(remoteClassName);

    CtClass[] interfaces = {pool.getCtClass(clazz.getName()), pool.getCtClass(RemoteInterface.class.getName())};
    remoteCtClass.setInterfaces(interfaces);

    // 添加私有成员app
    CtField appField = new CtField(pool.get(App.class.getName()), "app", remoteCtClass);
    appField.setModifiers(Modifier.PRIVATE | Modifier.FINAL);
    remoteCtClass.addField(appField);

    // 添加get方法
    remoteCtClass.addMethod(CtNewMethod.getter("getApp", appField));

    // 添加有参的构造函数
    CtConstructor constructor1 = new CtConstructor(new CtClass[]{pool.get(App.class.getName())}, remoteCtClass);
    constructor1.setBody("{$0.app = $1;}");
    remoteCtClass.addConstructor(constructor1);

    for (MethodConfig config : configs) {
      Method method = config.method;
      Class<? extends MethodParam> methodParamClass = MethodParamClassFactory.createClass(method);
      long timeout = config.timeout;

      // 添加私有成员failoverInvoker
      String failoverFieldName = getFailoverInvokerFieldName(app, method);
      CtField failoverField = new CtField(pool.get(Invoker.class.getName()), failoverFieldName, remoteCtClass);
      appField.setModifiers(Modifier.PRIVATE);
      remoteCtClass.addField(failoverField);

      StringBuilder methodBuilder = new StringBuilder();

      methodBuilder.append("public ");
      methodBuilder.append(method.getReturnType().getName());
      methodBuilder.append(" ");
      methodBuilder.append(method.getName());
      methodBuilder.append("(");

      Class<?>[] parameterTypes = method.getParameterTypes();
      for (int i = 0; i < parameterTypes.length; i++) {
        Class<?> parameterType = parameterTypes[i];

        methodBuilder.append(parameterType.getName());
        methodBuilder.append(" param");
        methodBuilder.append(i);

        if (i != parameterTypes.length - 1) {
          methodBuilder.append(", ");
        }
      }

      methodBuilder.append("){\r\n");

      methodBuilder.append("  return ");

      if (config.ignore) {
        if (log.isInfoEnabled()) {
          log.info(InvokerUtils.getServiceMethodName(app.group, app.app, config.method) + " ignore");
        }

        methodBuilder.append("$remote_ignore()");
      } else {
        if (log.isInfoEnabled()) {
          log.info(InvokerUtils.getServiceMethodName(app.group, app.app, config.method) //
                  + " register, config:" + config);
        }

        methodBuilder.append("$remote_execute(");

        methodBuilder.append(app.getMethodId(method));
        methodBuilder.append(", ");
        methodBuilder.append(timeout);
        methodBuilder.append("L, ");
        methodBuilder.append("new ");
        methodBuilder.append(methodParamClass.getName());
        methodBuilder.append("(");

        for (int i = 0; i < parameterTypes.length; i++) {
          methodBuilder.append("param");
          methodBuilder.append(i);

          if (i != parameterTypes.length - 1) {
            methodBuilder.append(",");
          }
        }

        methodBuilder.append("), ");

        methodBuilder.append(failoverFieldName);
        methodBuilder.append(")");
      }

      methodBuilder.append(";\r\n}");

      CtMethod m = CtNewMethod.make(methodBuilder.toString(), remoteCtClass);
      remoteCtClass.addMethod(m);
    }

    Class<?> invokerClass = remoteCtClass.toClass();

    return invokerClass.getConstructor(App.class).newInstance(app);
  }

  public void setFailover(App app, Class<?> clazz, Object service, Object failover) {
    // 设置failoverInvoker
    failoverInvokerFactory.register(clazz, failover);
    Method[] allMethods = clazz.getMethods();

    for (Method method : allMethods) {
      if (!Modifier.isPublic(method.getModifiers())) {
        continue;
      }

      if (Modifier.isStatic(method.getModifiers())) {
        continue;
      }

      Integer methodId = app.getMethodId(method);

      if (methodId == null) {
        continue;
      }

      Invoker<CompletableFuture<?>> failoverInvoker = failoverInvokerFactory.get(method);

      setFailoverInvoker(app, method, service, failoverInvoker);
    }
  }

  private String getFailoverInvokerFieldName(App app, Method method) {
    Integer methodId = app.getMethodId(method);

    if (methodId == null) {
      String msg = "cannot get the methodId, " + InvokerUtils.getServiceMethodName(app.group, app.app, method);
      throw new RuntimeException(msg);
    }

    return "$failoverInvoker_" + methodId;
  }

  private void setFailoverInvoker(App app, Method method, Object service,
                                  Invoker<CompletableFuture<?>> failoverInvoker) {
    try {
      String failoverFieldName = getFailoverInvokerFieldName(app, method);
      Field failoverField = service.getClass().getDeclaredField(failoverFieldName);
      failoverField.setAccessible(true);
      failoverField.set(service, failoverInvoker);
    } catch (Exception e) {
      throw new RuntimeException(e);
    }
  }

}
